OO_U4 - UML
本文使用Python搭建评测机。使用其他语言,在具体实现上可能存在差异,本文只是提供了一个思路。
前言
众所周知,本次作业采用了“交互式评测”,会根据同学们的程序输出,来动态生成新的指令。这对本地复现作业评测方式,构建自动化测试带来了一些挑战。
本人在一番折腾,并在各路大模型的协助后,成功在本地初步完成了交互式评测的搭建。本文将对我折腾的经验,进行一些简要的分享。
数据生成
正如评测说明,对于每一次评测,我们都有一套“基础输入”,取书/还书请求根据程序运行结果动态生成。
因此,我们采取如下的思路:
- 数据生成器负责生成“基础输入”
- 取书/还书请求由Checker根据程序运行结果,在基础输入中选取合适位置插入
Checker此时相当于承担了一部分数据生成的工作。
当然,为了方便复现测试结果,建议Checker输出一份stdin_actual,记录下插入了还书/取书请求的实际输入。
评测框架的修改
在前几单元中,我的评测机模式是这样,相信大家在这部分大同小异:
- 评测机框架+数据生成器+Checker;
- 数据生成器一次生成所有测试输入
- Checker在待测程序完全运行后启动;通过比较待测程序完全运行后的结果,与Checker预期结果,得出待测程序是否正确的判断
在本单元的交互式评测模式下,类似第二单元中,使用数据投喂器的思路,我们:
- 将Checker与待测程序同时运行
- 用管道将二者的输入、输出连接起来
- 待测程序的输入由Checker进行投放:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19java_process = subprocess.Popen(
['java', '-jar', os.path.join(target_dir, jar_file), f"-Xmx{memLimit}"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=fileerr,
)
check_process = subprocess.Popen(
["python3", checker_path, out_file], # 注意参数out_file
cwd=case_dir,
stdin=java_process.stdout,
stdout=java_process.stdin,
stderr=logfile,
)
# 在完成数据投递/正确检查后,必须显式关闭java_process的stdin
# 否则待测程序会认为仍有输入待读,一直等待,无法结束
check_process.wait()
java_process.stdin.close()
在上面的代码示例中,我的Checker通过读取工作目录下的stdin.txt,获取基础输入。因此,check_process中通过cwd指定了Checker的工作目录,以便进行多样例的并发评测。
进行自动化测试时,我们当然想要知道程序的输出;由于待测程序的输出管道接到了Checker的输入管道上,我们能想到的最美观办法,自然是由Checker记录待测程序的实际输出。
Checker的修改
上面我们已经提到了这几个额外需求:
- 动态生成return/pick指令,并插入到基础输入的特定位置
- 记录实际输入
- 记录程序输出
还有Checker本身要做的:
- 交互式评测:向程序输入指令,同时动态获取一行或多行输出
- 正确性检查
动态获取输出
这是Checker这几点中最麻烦的部分,主要在于多行输出的处理。
上面我们已经将Checker与待测程序的输入、输出管道连接在了一起。在Checker处,获取一行待测程序的输出,只需要sys.stdin.readline()即可。
那么,对于查询轨迹这种有多行输出的情况,是不是sys.stdin.readlines()就可以了呢?
很遗憾,是不行的。 因为readlines()是以EOF判断读取的结尾的,而EOF一般情况只在管道关闭时才能发送。那怎么办呢?
观察输出格式,不难发现,对于多行输出的情况,输出的第一行包含了我们接下来输出的行数。
1 | query: |
因此,我们只需要想办法提取出接下来的行数”n”,随后n次readline() 即可。相信这也是课程组官方的交互式评测机处理多行输出的方法。
插入还/取书指令
上面提到,我们的OPEN/CLOSE等基础指令,是由数据生成器生成的。为了简化问题,我们规定,插入的还/取书操作,都在已有的OPEN/CLOSE间插入,不新开OPEN/CLOSE对。
在这个规定下,我们需要做这几件事情:
- 读取生成的
stdin.txt,得知所有OPEN/CLOSE对的时间 - 写一个函数,对于一个成功的预约/借阅请求,取一个预约/借阅日之后的开馆日,作为插入还/取书指令的时间
- 插入请求
对于如何插入请求,我的做法是这样 感谢D指导提供思路 :
- 两个队列,队列A记录
stdin.txt中的初始指令,队列B记录要插入的指令 - 队列中元素是一个三元组:
(date,priority,line),其中,OPEN、其他指令、CLOSE的优先级分别为0、1、2 - 每次要插入指令时,把要插入的指令存在队列B中;随后,将两个队列合并,并依次按照日期、优先级的顺序重新排序
- Checker从合并好的队列中,取一条指令,作为发送给待测程序的输入
如此我们就完成了指令的插入,剩下的事情就很简单了。
记录程序输出/实际输入
记录实际输入,只需要在向待测程序发送输入时,在stdin_actual里记下来即可。
同理,对于实际输出,读出一行,向out.txt里写一行就是。
下面仅作示意。
1 | with open("./stdin_actual.txt", 'w') as actualStdin, \ |
正确性检查
这一部分,就可以像前几个单元一样,交给AI代劳了。
本人对Deepseek-R1,Gemini-2.5 Pro在内的多个模型进行测试,发现其均无法一下生成可用的交互式Checker(比如说,上面readlines()的问题,我没有特殊提示,AI很难一下处理好)。
这里建议,按照上面的步骤完成了输入/输出的处理后,再交给AI完成正确性检查的部分。
本次指导书的正确性约束较为分散,建议在给模型输入指导书时,在提示词里明确给出正确性约束条件,比如:
- 借书/预约/取书成功的条件
- 每次操作,书的运动轨迹:如借书时,从书架到用户
下面给出一个我用的prompt,供各位参考。需要注意,这个prompt我没有显式给出正确性约束条件,我得到的生成效果因此不大理想:
1 | 请按照content.json中的指导书,帮我修改本次作业的正确性检查器(checker.py)。 |